﻿using System;
using System.Collections.Generic;
using System.Collections;

namespace Picnic
{
    /// <summary>
    /// Represents an equals bilder.
    /// </summary>
    /// <typeparam name="T">A type of objects which should be compared.</typeparam>
    /// <example>
    /// var areEquals = EqualsBuilder&lt;Person&gt;
    ///     .Are(person1, person2)
    ///     .EqualsByType()
    ///     .EqualsBy(p => p.FirstName, true)
    ///     .EqualsBy(p => p.SecondName, true)
    ///     .EqualsBy(p => p.BirthDate)
    ///     .GetResult();
    /// </example>
    public class EqualsBuilder<T> : IEqualityChain<T> where T : class
    {
        private T object1;
        private T object2;
        private bool? result;

        private EqualsBuilder(T object1, T object2, bool? result)
        {
            this.object1 = object1;
            this.object2 = object2;
            this.result = result;
        }

        /// <summary>
        /// Begins equality chain.
        /// </summary>
        public static IEqualityChain<T> Are(T object1, T object2)
        {
            if (ReferenceEquals(object1, object2))
            {
                return new EqualsBuilder<T>(object1, object2, true);
            }

            if (ReferenceEquals(object1, null) || ReferenceEquals(object2, null))
            {
                return new EqualsBuilder<T>(object1, object2, false);
            }

            return new EqualsBuilder<T>(object1, object2, null);
        }

        public IEqualityChain<T> EqualsByType()
        {
            if (result.HasValue)
                return this;

            var type1 = object1.GetType();
            var type2 = object2.GetType();

            return GetNextChainLink(type1 == type2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, string> valueProvider)
        {
            return EqualsBy(valueProvider, false);
        }

        public IEqualityChain<T> EqualsBy(Func<T, string> valueProvider, bool ignoreCase)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);
            var comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

            return GetNextChainLink(string.Equals(value1, value2, comparison));
        }

        public IEqualityChain<T> EqualsBy(Func<T, int> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            return GetNextChainLink(value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, long> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            return GetNextChainLink(value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, decimal> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            return GetNextChainLink(value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, float> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            return GetNextChainLink(value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, double> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            return GetNextChainLink(value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, bool> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            return GetNextChainLink(value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, DateTime> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            return GetNextChainLink(value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, TimeSpan> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            return GetNextChainLink(value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, object> valueProvider)
        {
            if (result.HasValue)
                return this;

            var value1 = valueProvider(object1);
            var value2 = valueProvider(object2);

            if (ReferenceEquals(value1, value2))
                return new EqualsBuilder<T>(object1, object2, null);

            if (ReferenceEquals(value1, null) || ReferenceEquals(value2, null))
                return new EqualsBuilder<T>(object1, object2, false);

            return GetNextChainLink(value1.Equals(value2));
        }

        public IEqualityChain<T> EqualsBy(Func<T, ICollection<string>> valueProvider)
        {
            return EqualsBy(valueProvider, false);
        }

        public IEqualityChain<T> EqualsBy(Func<T, ICollection<string>> valueProvider, bool ignoreCase)
        {
            var comparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

            return EqualsByCollection<string>(
                valueProvider(object1),
                valueProvider(object2),
                (value1, value2) => string.Equals(value1, value2, comparison));
        }

        public IEqualityChain<T> EqualsBy(Func<T, ICollection<int>> valueProvider)
        {
            return EqualsByCollection<int>(
                valueProvider(object1),
                valueProvider(object2),
                (value1, value2) => value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, ICollection<long>> valueProvider)
        {
            return EqualsByCollection<long>(
                valueProvider(object1),
                valueProvider(object2),
                (value1, value2) => value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, ICollection<decimal>> valueProvider)
        {
            return EqualsByCollection<decimal>(
                valueProvider(object1),
                valueProvider(object2),
                (value1, value2) => value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, ICollection<double>> valueProvider)
        {
            return EqualsByCollection<double>(
                valueProvider(object1),
                valueProvider(object2),
                (value1, value2) => value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, ICollection<float>> valueProvider)
        {
            return EqualsByCollection<float>(
                valueProvider(object1),
                valueProvider(object2),
                (value1, value2) => value1 == value2);
        }

        public IEqualityChain<T> EqualsBy(Func<T, ICollection<bool>> valueProvider)
        {
            return EqualsByCollection<bool>(
                valueProvider(object1),
                valueProvider(object2),
                (value1, value2) => value1 == value2);
        }

        public bool GetResult()
        {
            return result.HasValue ? result.Value : true;
        }

        private IEqualityChain<T> GetNextChainLink(bool condition)
        {
            if (condition)
            {
                return this;
            }

            return new EqualsBuilder<T>(object1, object2, false);
        }

        private IEqualityChain<T> EqualsByCollection<TValue>(
            ICollection<TValue> collection1,
            ICollection<TValue> collection2,
            Func<TValue, TValue, bool> comparer)
        {
            if (result.HasValue)
                return this;

            if (collection1 == collection2)
            {
                return this;
            }

            if (collection1 == null || collection2 == null)
            {
                return new EqualsBuilder<T>(object1, object2, false);
            }

            if (collection1.Count != collection2.Count)
            {
                return new EqualsBuilder<T>(object1, object2, false);
            }

            var iterator1 = collection1.GetEnumerator();
            var iterator2 = collection2.GetEnumerator();

            for (var i = 0; i < collection1.Count; i++)
            {
                iterator1.MoveNext();
                iterator2.MoveNext();

                if (!comparer(iterator1.Current, iterator2.Current))
                {
                    return new EqualsBuilder<T>(object1, object2, false);
                }
            }

            return this;
        }
    }
} 
